package com.hbfec.fileserver.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hbfec.fileserver.callback.CustomDownloadCallback;
import com.hbfec.fileserver.constant.CacheKeys;
import com.hbfec.fileserver.constant.ErrorCode;
import com.hbfec.fileserver.constant.FileStatus;
import com.hbfec.fileserver.constant.Tag;
import com.hbfec.fileserver.json.Response;
import com.hbfec.fileserver.service.CacheService;
import com.hbfec.fileserver.service.DbService;
import com.hbfec.fileserver.service.FastDFSService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Map;

/**
 * 文件下载Controller
 */
@RestController
public class DownloadController {
    private static final Logger LOGGER = LoggerFactory.getLogger(DownloadController.class);

    @Value("${fileserver.charset}")
    private String charset;
    @Value("${fileserver.exist.valid-freq}")
    private Long validFreq;
    @Value("${fileserver.exist.valid-count-limit}")
    private Integer validCountLimit;

    @Autowired
    private DbService dbService;
    @Autowired
    private CacheService cacheService;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private FastDFSService fastDFSService;

    /**
     * 文件下载
     *
     * @param uuid     文件的唯一标识
     * @param request  request
     * @param response response
     */
    @RequestMapping(value = "/file/{uuid}", method = RequestMethod.GET)
    public void download(@PathVariable(value = "uuid", required = true) String uuid,
                         HttpServletRequest request, HttpServletResponse response) {
        try (OutputStream os = response.getOutputStream()) {
            // 设置失败应答头
            setResponseHeader(response);
            // 验证文件信息是否还在缓存中，没有入库
            try {
                boolean exist = cacheService.exists(
                        String.format("%s%s_%s", CacheKeys.FILE_STATUS, uuid, FileStatus.UPLOADED),
                        validCountLimit, validFreq);
                if (exist) {
                    respond(os, Tag.FAILURE, ErrorCode.TIME_OUT, "服务忙，请稍后重试", null);
                    return;
                }
            } catch (Exception cacheEx) {
                respond(os, Tag.FAILURE, ErrorCode.UNKNOWN_ERROR, "验证缓存中是否存在文件信息时出现异常", null);
                return;
            }
            // 根据uuid获取文件详情
            Map<String, Object> fileDetail = null;
            try {
                fileDetail = dbService.queryFileWithUuidAndStatus(uuid, FileStatus.UPLOADED);
            } catch (EmptyResultDataAccessException emptyResultEX) {
                respond(os, Tag.FAILURE, ErrorCode.SPECIFIED_OBJECT_CANNOT_BE_FOUND, "文件不存在，下载失败", null);
                return;
            } catch (Exception dbEx) {
                respond(os, Tag.FAILURE, ErrorCode.DATABASE_ERROR, "数据库中查询文件发生异常", null);
                return;
            }
            // 设置成功应答头
            setResponseHeader(response, (Long) fileDetail.get("size"), (String) fileDetail.get("name"));
            // 解析url
            String[] urlParts = ((String) fileDetail.get("url")).split("/", 3);
            // 开始下载
            if (fastDFSService.downloadFile(urlParts[1], urlParts[2], new CustomDownloadCallback(os)) < 0) {
                try {
                    setResponseHeaderAndRespond(response, os,
                            Tag.FAILURE, ErrorCode.UNKNOWN_ERROR, "从FastDFS获取文件失败", null);
                } catch (Exception ex) {
                    // 用户点击取消下载后会导致连接关闭，从而write时会报错，忽略该种情况
                }
                return;
            }
        } catch (IOException ex) {
            LOGGER.error("获取输出流或写入输出流时出现异常，直接关闭连接", ex);
        } catch (Exception ex) {
            LOGGER.error("在解析数据时出现异常，直接关闭连接", ex);
        }
    }

    /**
     * 设置失败结果应答头，在初始时设置
     *
     * @param servletResponse servletResponse
     * @throws Exception Exception
     */
    private void setResponseHeader(HttpServletResponse servletResponse) throws Exception {
        servletResponse.setContentType(String.format("application/json;charset=%s", charset));
    }

    /**
     * 设置成功结果应答头
     *
     * @param servletResponse servletResponse
     * @param len             返回的文件的字节长度
     * @param fileName        文件名
     * @throws Exception Exception
     */
    private void setResponseHeader(HttpServletResponse servletResponse, long len, String fileName) throws Exception {
        servletResponse.setContentType("application/octet-stream");
        servletResponse.setContentLengthLong(len);
        servletResponse.setHeader("Content-Disposition",
                "attachment;filename*=" + charset + "''" + URLEncoder.encode(fileName, charset));
    }


    /**
     * 返回失败结果
     *
     * @param os     输出流
     * @param tag    tag
     * @param code   code
     * @param reason reason
     * @param result result
     * @throws Exception Exception
     */
    private void respond(OutputStream os, int tag, int code, String reason, Object result) throws Exception {
        Response response = new Response(tag, code, reason, result);
        os.write(objectMapper.writeValueAsString(response).getBytes(charset));
    }


    /**
     * 设置失败结果应答头，并返回失败结果
     *
     * @param servletResponse servletResponse
     * @param os              输出流
     * @param tag             tag
     * @param code            code
     * @param reason          reason
     * @param result          result
     * @throws Exception Exception
     */
    private void setResponseHeaderAndRespond(HttpServletResponse servletResponse, OutputStream os,
                                             int tag, int code, String reason, Object result) throws Exception {
        Response response = new Response(tag, code, reason, result);
        byte[] responseByteArr = objectMapper.writeValueAsString(response).getBytes(charset);
        servletResponse.setContentType(String.format("application/json;charset=%s", charset));
        servletResponse.setContentLength(responseByteArr.length);
        servletResponse.setHeader("Content-Disposition", "inline");
        os.write(responseByteArr);
    }
}
